﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TestXNA3.Resources;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using TestXNA3.Helpers;
using TestXNA3.Resources.Meshes;

namespace TestXNA3.Render
{
    public class SceneRenderer
    {
        // Debug lines
        public struct SceneRendererDebugLine
        {
            public SceneRendererDebugLine(Vector3 lineStart, Vector3 lineEnd, Color color, bool foreground)
            {
                LineStart = lineStart;
                LineEnd = lineEnd;
                Color = color;
                Foreground = foreground;
            }
            public Vector3 LineStart;
            public Vector3 LineEnd;
            public Color Color;
            public bool Foreground;
        }

        public static List<SceneRendererDebugLine> PersistentLines = new List<SceneRendererDebugLine>(2048);
        public static List<SceneRendererDebugLine> OneFrameLines = new List<SceneRendererDebugLine>(2048);
        public static void DrawDebugBox(BoundingBox box, Color color, bool persistent, bool foreground)
        {
            Vector3[] corners = 
            {
                box.Min,
                box.Min + new Vector3(box.Max.X - box.Min.X, 0, 0),
                box.Min + new Vector3(0, box.Max.Y - box.Min.Y, 0),
                box.Min + new Vector3(0, 0, box.Max.Z - box.Min.Z),
                
                box.Max,
                box.Max - new Vector3(box.Max.X - box.Min.X, 0, 0),
                box.Max - new Vector3(0, box.Max.Y - box.Min.Y, 0),
                box.Max - new Vector3(0, 0, box.Max.Z - box.Min.Z),
                
                box.Min + new Vector3(0, box.Max.Y - box.Min.Y, box.Max.Z - box.Min.Z),
                box.Min + new Vector3(box.Max.X - box.Min.X, 0, box.Max.Z - box.Min.Z),
                box.Min + new Vector3(box.Max.X - box.Min.X, box.Max.Y - box.Min.Y, 0)
            };

            List<SceneRendererDebugLine> debugLineList = (persistent) ? PersistentLines : OneFrameLines;

            debugLineList.Add(new SceneRendererDebugLine(corners[0], corners[1], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[0], corners[2], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[0], corners[3], color, foreground));

            debugLineList.Add(new SceneRendererDebugLine(corners[4], corners[5], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[4], corners[6], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[4], corners[7], color, foreground));

            debugLineList.Add(new SceneRendererDebugLine(corners[5], corners[2], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[6], corners[1], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[7], corners[2], color, foreground));

            debugLineList.Add(new SceneRendererDebugLine(corners[6], corners[3], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[3], corners[8], color, foreground));
            debugLineList.Add(new SceneRendererDebugLine(corners[7], corners[1], color, foreground));
        }

        public static void DrawDebugLine(Vector3 start, Vector3 end, Color color, bool persistent, bool foreground)
        {
            List<SceneRendererDebugLine> debugLineList = (persistent) ? PersistentLines : OneFrameLines;
            debugLineList.Add(new SceneRendererDebugLine(start, end, color, foreground));
        }

        public struct ViewerDesc
        {
            public float Fov;
            public float AspectRatio;
            public float NearClip;
            public float FarClip;
            public BoundingFrustum Frustum;
            public BoundingBox MeshesBox;
            public BoundingBox ShadowBox; 
            public Matrix View;
            public Matrix Projection;
            public Vector3 Location;
            public Vector3 Forward;
            public Vector3 Up;
            public Vector3 Right;
            public List<MeshResource> MeshesToRender;
        }

        // Debug renderer
        private BasicEffect _debugLineEffect;

        // deferred shading RTs
        private RenderTarget2D _colorRT;
        private RenderTarget2D _normalEmissiveRT;
        private RenderTarget2D _depthRT;
        private RenderTarget2D _accumulatedLightingRT;
        private RenderTarget2D _bloomBuffer;
        private RenderTarget2D _bloomTempBuffer;

        private RenderTarget2D _shadowMap;
        private RenderTarget2D _blurredShadowMap;

        // deferred shading effects
        private Effect _clearGBuffer;
        private Effect _bloomEffect;
        private Effect _finalGatherEffect;

        private Effect _pointLightEffect;
        private Effect _directionalLightEffect;
        private Effect _directionalLightShadowEffect;
        private Effect _skyLightEffect;

        private Effect _boxBlurEffect;

        private Model _sphereModel;

        // Helps to remove per frame allocs
        private VertexDeclaration _debugLinesVertexDecl;
        private BoundingFrustum _workingFrustum;

        // View matrix
        private Matrix _view;
        public Matrix View
        {
            get { return _view; }
        }

        // Projection matrix
        private Matrix _projection;
        public Matrix Projection
        {
            get { return _projection; }
        }

        public enum ELightType
        {
            Point,
            Spot,
            Directional,
            Sky
        };
        public struct LightDesc
        {
            public ELightType Type;

            public bool CastShadows;

            public Color Diffuse;
            public Color Specular;
            public float Intensity;

            public Vector3 Location;
            public Vector3 Direction;
            public Vector3 Up;

            public float Fov;

            public float Far;
            public float Near;

            // Point
            public float Radius;

            // Spot (not implemented)
            //float InnerCone;
            //float OuterCone;

            // Directional

            // Sky Light
        };
        private List<LightDesc> _lights;

        public enum EDrawMode
        {
            Default,
            DepthOnly
        }

        public SceneRenderer()
        {
            _clearGBuffer = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/ClearGBuffer");
            _finalGatherEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/FinalGather");
            _pointLightEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/PointLight");

            _directionalLightEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/DirectionalLight");
            _directionalLightShadowEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/DirectionalLightShadow");

            _skyLightEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/SkyLight");

            _sphereModel = GameStartup.ContentManager.Load<Model>("StaticMeshes/sphere");

            _boxBlurEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/Blur");
            _bloomEffect = GameStartup.ContentManager.Load<Effect>("Shaders/DeferredShading/Bloom");

            _lights = new List<LightDesc>();

            _debugLineEffect = new BasicEffect(GameStartup.Graphics, null);
            _debugLineEffect.VertexColorEnabled = true;

            _workingFrustum = new BoundingFrustum(Matrix.Identity);
        }

        private int Width = 0;
        private int Height = 0;
        private void CheckRT()
        {
            if (Width != GameStartup.Width || Height != GameStartup.Height)
            {
                Width = GameStartup.Width;
                Height = GameStartup.Height;

                _colorRT = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width,
                                                                        GameStartup.Height, 1, SurfaceFormat.Color);
                _normalEmissiveRT = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width,
                                                                        GameStartup.Height, 1, SurfaceFormat.Color);
                _depthRT = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width,
                                                                        GameStartup.Height, 1, SurfaceFormat.Single);

//                _depthRT.GetTexture().GetData<float>(

                _shadowMap = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width, GameStartup.Height, 1, SurfaceFormat.Vector2);

                _blurredShadowMap = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width,
                                                                        GameStartup.Height, 1, SurfaceFormat.Vector2);

                _accumulatedLightingRT = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width,
                                                                        GameStartup.Height, 1, SurfaceFormat.HalfVector4, RenderTargetUsage.PreserveContents);

                _bloomBuffer = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width / 4,
                                                                        GameStartup.Height / 4, 1, SurfaceFormat.HalfVector4);

                _bloomTempBuffer = new RenderTarget2D(GameStartup.Graphics, GameStartup.Width / 4,
                                                                        GameStartup.Height / 4, 1, SurfaceFormat.HalfVector4);
            }
        }

        public void PushLight(ref LightDesc lightDesc)
        {
            _lights.Add(lightDesc);
        }

        private void DrawMesh(MeshResource m, Vector3 viewerLocation)
        {
            m.Render(ref _view, ref _projection, ref viewerLocation, EDrawMode.Default);
        }

        private void DrawMesh(MeshResource m, Vector3 viewerLocation, ref Matrix view, ref Matrix projection, EDrawMode drawMode)
        {
            m.Render(ref view, ref projection, ref viewerLocation, drawMode);
        }

        private Color Int32ToColor(int color)
        {
            byte a = (byte)((color & 0xFF000000) >> 32);
            byte r = (byte)((color & 0x00FF0000) >> 16);
            byte g = (byte)((color & 0x0000FF00) >> 8);
            byte b = (byte)((color & 0x000000FF) >> 0);

            return new Color(r, g, b, a);
        }

        private void DrawDebugLines(bool drawForegroundLines)
        {
            if ((OneFrameLines.Count + PersistentLines.Count) == 0)
                return;

            _debugLineEffect.World = Matrix.Identity;
            _debugLineEffect.View = _view;
            _debugLineEffect.Projection = _projection;

            if (_debugLinesVertexDecl == null)
            {
                _debugLinesVertexDecl = new VertexDeclaration(GameStartup.Graphics, VertexPositionColor.VertexElements);
            }

            GameStartup.Graphics.VertexDeclaration = _debugLinesVertexDecl;
            //StillDesign.PhysX.DebugRenderable data = GameStartup.StaticGameStartup.GameInfo.Scene.PhysicsScene.GetDebugRenderable();

            _debugLineEffect.Begin();

            foreach (EffectPass pass in _debugLineEffect.CurrentTechnique.Passes)
            {
                pass.Begin();

                if (OneFrameLines.Count > 0)
                {
                    VertexPositionColor[] vertices = new VertexPositionColor[OneFrameLines.Count * 2];
                    for (int x = 0; x < OneFrameLines.Count; x++)
                    {
                        if (OneFrameLines[x].Foreground == drawForegroundLines)
                        {
                            vertices[x * 2 + 0] = new VertexPositionColor(OneFrameLines[x].LineStart, OneFrameLines[x].Color);
                            vertices[x * 2 + 1] = new VertexPositionColor(OneFrameLines[x].LineEnd, OneFrameLines[x].Color);
                        }
                    }

                    GameStartup.Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, vertices, 0, OneFrameLines.Count);
                }

                if (PersistentLines.Count > 0)
                {
                    VertexPositionColor[] vertices = new VertexPositionColor[PersistentLines.Count * 2];
                    for (int x = 0; x < PersistentLines.Count; x++)
                    {
                        if (PersistentLines[x].Foreground == drawForegroundLines)
                        {
                            vertices[x * 2 + 0] = new VertexPositionColor(PersistentLines[x].LineStart, PersistentLines[x].Color);
                            vertices[x * 2 + 1] = new VertexPositionColor(PersistentLines[x].LineEnd, PersistentLines[x].Color);
                        }
                    }

                    GameStartup.Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, vertices, 0, PersistentLines.Count);
                }

                /*if (data.PointCount > 0)
                {
                    StillDesign.PhysX.DebugPoint[] points = data.GetDebugPoints();

                    GameStartup.Graphics.DrawUserPrimitives<StillDesign.PhysX.DebugPoint>(PrimitiveType.PointList, points, 0, points.Length);
                }

                if (data.LineCount > 0)
                {
                    StillDesign.PhysX.DebugLine[] lines = data.GetDebugLines();

                    VertexPositionColor[] vertices = new VertexPositionColor[data.LineCount * 2];
                    for (int x = 0; x < data.LineCount; x++)
                    {
                        StillDesign.PhysX.DebugLine line = lines[x];

                        vertices[x * 2 + 0] = new VertexPositionColor(line.Point0, Int32ToColor(line.Color));
                        vertices[x * 2 + 1] = new VertexPositionColor(line.Point1, Int32ToColor(line.Color));
                    }

                    GameStartup.Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, vertices, 0, lines.Length);
                }

                if (data.TriangleCount > 0)
                {
                    StillDesign.PhysX.DebugTriangle[] triangles = data.GetDebugTriangles();

                    VertexPositionColor[] vertices = new VertexPositionColor[data.TriangleCount * 3];
                    for (int x = 0; x < data.TriangleCount; x++)
                    {
                        StillDesign.PhysX.DebugTriangle triangle = triangles[x];

                        vertices[x * 3 + 0] = new VertexPositionColor(triangle.Point0, Int32ToColor(triangle.Color));
                        vertices[x * 3 + 1] = new VertexPositionColor(triangle.Point1, Int32ToColor(triangle.Color));
                        vertices[x * 3 + 2] = new VertexPositionColor(triangle.Point2, Int32ToColor(triangle.Color));
                    }

                    GameStartup.Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, vertices, 0, triangles.Length);
                }*/

                pass.End();
            }

            _debugLineEffect.End();
        }

        // Draw meshes
        public void Draw(ref ViewerDesc viewerDesc)
        {
            for (int i = 0; i < viewerDesc.MeshesToRender.Count; i++)
            {
                viewerDesc.MeshesBox = BoundingBox.CreateMerged(viewerDesc.MeshesBox, BoundingBox.CreateFromSphere(viewerDesc.MeshesToRender[i].BoundingSphere));
                if (viewerDesc.MeshesToRender[i].CastShadows)
                {
                    viewerDesc.ShadowBox = BoundingBox.CreateMerged(viewerDesc.MeshesBox, BoundingBox.CreateFromSphere(viewerDesc.MeshesToRender[i].BoundingSphere));
                }
            }
            DrawDebugBox(viewerDesc.ShadowBox, Color.Azure, false, false);

            CheckRT();

            // Calculate matrices
            _view = viewerDesc.View;
            _projection = viewerDesc.Projection;

            // Fill G buffer
            {
                // Set deferred shading RTs
                GameStartup.Graphics.SetRenderTarget(0, _colorRT);
                GameStartup.Graphics.SetRenderTarget(1, _normalEmissiveRT);
                GameStartup.Graphics.SetRenderTarget(2, _depthRT);

                // Clear all RTs
                GameStartup.Graphics.Clear(Color.Black);
                _clearGBuffer.Begin();
                _clearGBuffer.Techniques[0].Passes[0].Begin();

                GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);

                _clearGBuffer.Techniques[0].Passes[0].End();
                _clearGBuffer.End();

                GameStartup.Graphics.RenderState.DepthBufferEnable = true;

                // Draw scene
                foreach (MeshResource m in viewerDesc.MeshesToRender)
                {
                    if (viewerDesc.Frustum.Contains(m.BoundingSphere) != ContainmentType.Disjoint)
                    {
                        DrawMesh(m, viewerDesc.Location);
                    }
                    else
                    {
                        if (m is Sprite)
                        {
                        }
                    }
                }

                // Draw debug lines
                DrawDebugLines(false);
            }

            // Switch to forward rendering
            Vector2 halfPixel = new Vector2(0.5f / (float)GameStartup.Width, 0.5f / (float)GameStartup.Height);
            GameStartup.Graphics.SetRenderTarget(0, null);
            GameStartup.Graphics.SetRenderTarget(1, null);
            GameStartup.Graphics.SetRenderTarget(2, null);

            // Lighting
            {
                GameStartup.Graphics.SetRenderTarget(0, _accumulatedLightingRT);
                GameStartup.Graphics.Clear(ClearOptions.Target, Color.Black, 1.0f, 0);

                // Enable additive blending
                GameStartup.Graphics.RenderState.AlphaBlendEnable = true;
                GameStartup.Graphics.RenderState.AlphaBlendOperation = BlendFunction.Add;
                GameStartup.Graphics.RenderState.SourceBlend = Blend.One;
                GameStartup.Graphics.RenderState.DestinationBlend = Blend.One;
                GameStartup.Graphics.RenderState.SeparateAlphaBlendEnabled = false;
                GameStartup.Graphics.RenderState.DepthBufferEnable = false;

                foreach (LightDesc desc in _lights)
                {
                    if (desc.Type == ELightType.Point)
                    {
                        Matrix sphereWorldMatrix = Matrix.CreateScale(desc.Radius) * Matrix.CreateTranslation(desc.Location);
                        _pointLightEffect.Parameters["View"].SetValue(_view);
                        _pointLightEffect.Parameters["Projection"].SetValue(_projection);
                        _pointLightEffect.Parameters["World"].SetValue(sphereWorldMatrix);

                        _pointLightEffect.Parameters["ColorRT"].SetValue(_colorRT.GetTexture());
                        _pointLightEffect.Parameters["NormalRT"].SetValue(_normalEmissiveRT.GetTexture());
                        _pointLightEffect.Parameters["DepthRT"].SetValue(_depthRT.GetTexture());
                        _pointLightEffect.Parameters["cameraPosition"].SetValue(viewerDesc.Location);
                        _pointLightEffect.Parameters["diffuseColor"].SetValue(desc.Diffuse.ToVector3());
                        _pointLightEffect.Parameters["lightIntensity"].SetValue(desc.Intensity);
                        _pointLightEffect.Parameters["lightRadius"].SetValue(desc.Radius);
                        _pointLightEffect.Parameters["lightPosition"].SetValue(desc.Location);
                        _pointLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(_view * _projection));
                        _pointLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

                        float cameraToCenter = Vector3.Distance(viewerDesc.Location, desc.Location);
                        if (cameraToCenter < desc.Radius)
                            GameStartup.Graphics.RenderState.CullMode = CullMode.CullClockwiseFace;
                        else
                            GameStartup.Graphics.RenderState.CullMode = CullMode.CullCounterClockwiseFace;

                        _pointLightEffect.Begin();
                        _pointLightEffect.Techniques[0].Passes[0].Begin();

                        // Render occlusion sphere
                        {
                            foreach (ModelMesh mesh in _sphereModel.Meshes)
                            {
                                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                                {
                                    GameStartup.Graphics.VertexDeclaration = meshPart.VertexDeclaration;
                                    GameStartup.Graphics.Vertices[0].SetSource(mesh.VertexBuffer,
                                        meshPart.StreamOffset, meshPart.VertexStride);
                                    GameStartup.Graphics.Indices = mesh.IndexBuffer;
                                    GameStartup.Graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                        meshPart.BaseVertex, 0, meshPart.NumVertices,
                                        meshPart.StartIndex, meshPart.PrimitiveCount);
                                }
                            }
                        }

                        _pointLightEffect.Techniques[0].Passes[0].End();
                        _pointLightEffect.End();
                        GameStartup.Graphics.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
                    }
                    else
                        if (desc.Type == ELightType.Directional)
                        {
                            DrawDirectionalLight(desc, ref viewerDesc);
                        }
                        else
                            if (desc.Type == ELightType.Sky)
                            {
                                _skyLightEffect.Parameters["lightDirection"].SetValue(desc.Direction);
                                _skyLightEffect.Parameters["diffuseColor"].SetValue(desc.Diffuse.ToVector3());
                                _skyLightEffect.Parameters["lightIntensity"].SetValue(desc.Intensity);
                                _skyLightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(_view * _projection));
                                _skyLightEffect.Parameters["cameraPosition"].SetValue(viewerDesc.Location);

                                _skyLightEffect.Parameters["ColorRT"].SetValue(_colorRT.GetTexture());
                                _skyLightEffect.Parameters["NormalRT"].SetValue(_normalEmissiveRT.GetTexture());
                                _skyLightEffect.Parameters["DepthRT"].SetValue(_depthRT.GetTexture());

                                _skyLightEffect.Parameters["halfPixel"].SetValue(halfPixel);

                                _skyLightEffect.Begin();
                                _skyLightEffect.Techniques[0].Passes[0].Begin();
                                GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                                _skyLightEffect.Techniques[0].Passes[0].End();
                                _skyLightEffect.End();
                            }
                }

                GameStartup.Graphics.RenderState.AlphaBlendEnable = false;
                GameStartup.Graphics.RenderState.DepthBufferEnable = true;
            }

            // Bloom
            bool neverUseIt = false;
            if (neverUseIt)
            {
                // clip
                GameStartup.Graphics.SetRenderTarget(0, _bloomTempBuffer);
                _bloomEffect.CurrentTechnique = _bloomEffect.Techniques[0];

                _bloomEffect.Parameters["OriginalMap"].SetValue(_accumulatedLightingRT.GetTexture());
                halfPixel = new Vector2(0.5f / _bloomBuffer.Width, 0.5f / _bloomBuffer.Height);
                _bloomEffect.Parameters["halfPixel"].SetValue(halfPixel);
                _bloomEffect.Parameters["pixelWidth"].SetValue(halfPixel.X);
                _bloomEffect.Begin();
                _bloomEffect.Techniques[0].Passes[0].Begin();
                GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                _bloomEffect.Techniques[0].Passes[0].End();
                _bloomEffect.End();

                // blur
                bool useTemp = false;
                for (int i = 0; i < 2; i++)
                {
                    RenderTarget2D currentTarget = (useTemp) ? _bloomTempBuffer : _bloomBuffer;
                    RenderTarget2D originalTarget = (useTemp) ? _bloomBuffer : _bloomTempBuffer;

                    GameStartup.Graphics.SetRenderTarget(0, currentTarget);
                    GameStartup.Graphics.Clear(Color.Black);
                    _boxBlurEffect.CurrentTechnique = _boxBlurEffect.Techniques[2];

                    _boxBlurEffect.Parameters["OriginalMap"].SetValue(originalTarget.GetTexture());
                    halfPixel = new Vector2(0.5f / _bloomBuffer.Width, 0.5f / _bloomBuffer.Height);
                    _boxBlurEffect.Parameters["halfPixel"].SetValue(halfPixel);
                    _boxBlurEffect.Parameters["pixelWidth"].SetValue(2f / (float)GameStartup.Width);
                    _boxBlurEffect.Begin();
                    _boxBlurEffect.Techniques[2].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    _boxBlurEffect.Techniques[2].Passes[0].End();
                    _boxBlurEffect.End();

                    useTemp = !useTemp;
                }
            }

            GameStartup.Graphics.SetRenderTarget(0, null);

            // Final gather
            _finalGatherEffect.Parameters["ColorRT"].SetValue(_colorRT.GetTexture());
            _finalGatherEffect.Parameters["LightRT"].SetValue(_accumulatedLightingRT.GetTexture());
            _finalGatherEffect.Parameters["NormalRT"].SetValue(_normalEmissiveRT.GetTexture());
            _finalGatherEffect.Parameters["DepthRT"].SetValue(_depthRT.GetTexture());
            //if (false)
            //{
                //_finalGatherEffect.Parameters["BlurredScene"].SetValue(_bloomBuffer.GetTexture());
            //}
            _finalGatherEffect.Parameters["halfPixel"].SetValue(halfPixel);
            GameStartup.Graphics.RenderState.DepthBufferEnable = false;

            _finalGatherEffect.Begin();
            _finalGatherEffect.Techniques[0].Passes[0].Begin();

            GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);

            _finalGatherEffect.Techniques[0].Passes[0].End();
            _finalGatherEffect.End();

            // Clear all lights
            _lights.Clear();

            // Draw foreground debug lines
            DrawDebugLines(true);

            // Clear one frame lines
            OneFrameLines.Clear();
        }

        protected void CalculateLightForFrustum(out Matrix lightView, out Matrix lightProjection, Vector3[] frustumCorners, LightDesc light)
        {
            // calculate standard view and projection matrices for light 
            lightView = Matrix.CreateLookAt(light.Location, light.Location + light.Direction, light.Up);
            lightProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(light.Fov), _shadowMap.Width / _shadowMap.Height, light.Near, light.Far);

            // Next we will find the min and max values of the current
            // frustum split in lights post-projection space
            // (where coordinate range is from -1.0 to 1.0)
            float maxX = -Single.MaxValue;
            float maxY = -Single.MaxValue;
            float minX = Single.MaxValue;
            float minY = Single.MaxValue;
            float maxZ = 0;

            Matrix mLightViewProj = lightView * lightProjection;

            // for each corner point
            for (int i = 0; i < 8; i++)
            {
                // transform point
                Vector4 transformed = Vector4.Transform(frustumCorners[i], mLightViewProj);

                // project x and y
                transformed.X /= transformed.W;
                transformed.Y /= transformed.W;

                // find min and max values
                if (transformed.X > maxX)
                {
                    maxX = transformed.X;
                }

                if (transformed.Y > maxY)
                {
                    maxY = transformed.Y;
                }

                if (transformed.Y < minY)
                {
                    minY = transformed.Y;
                }

                if (transformed.X < minX)
                {
                    minX = transformed.X;
                }

                // find largest z distance
                if (transformed.Z > maxZ)
                {
                    maxZ = transformed.Z;
                }
            }

            // set values to valid range (post-projection)
            maxX = MathHelper.Clamp(maxX, -1.0f, 1.0f);
            maxY = MathHelper.Clamp(maxY, -1.0f, 1.0f);
            minX = MathHelper.Clamp(minX, -1.0f, 1.0f);
            minY = MathHelper.Clamp(minY, -1.0f, 1.0f);

            light.Far = maxZ + light.Far + 1.25f;

            // calculate split view and projection matrices for light 
            lightView = Matrix.CreateLookAt(light.Location, light.Location + light.Direction, light.Up);
            lightProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(light.Fov), 1, light.Near, light.Far);

            // Next we build a special matrix for cropping the lights view
            // to only contain points of the current frustum split
            float scaleX = 2.0f / (maxX - minX);
            float scaleY = 2.0f / (maxY - minY);

            float offsetX = -0.5f * (maxX + minX) * scaleX;
            float offsetY = -0.5f * (maxY + minY) * scaleY;

            Matrix lightCropView = new Matrix(1.0f, 0.0f, 0.0f, 0.0f,
                                              0.0f, 1.0f, 0.0f, 0.0f,
                                              0.0f, 0.0f, 1.0f, 0.0f,
                                              0.0f, 0.0f, 0.0f, 1.0f);
            lightCropView.M11 = scaleX;
            lightCropView.M22 = scaleY;
            lightCropView.M41 = offsetX;
            lightCropView.M42 = offsetY;

            // multiply the projection matrix with it
            lightProjection *= lightCropView;

            // finally modify projection matrix for linearized depth
            lightProjection.M33 /= light.Far;
            lightProjection.M43 /= light.Far;
        }

        private void DrawDirectionalLightNoShadows(LightDesc desc, ref ViewerDesc viewerDesc)
        {
            Effect lightEffect = null;
            lightEffect = _directionalLightEffect;

            lightEffect.Parameters["lightDirection"].SetValue(desc.Direction);
            lightEffect.Parameters["diffuseColor"].SetValue(desc.Diffuse.ToVector3());
            lightEffect.Parameters["lightIntensity"].SetValue(desc.Intensity);
            lightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(_view * _projection));
            lightEffect.Parameters["cameraPosition"].SetValue(viewerDesc.Location);

            Vector2 halfPixel = new Vector2(0.5f / _shadowMap.Width, 0.5f / _shadowMap.Height);
            lightEffect.Parameters["halfPixel"].SetValue(halfPixel);

            lightEffect.Parameters["ColorRT"].SetValue(_colorRT.GetTexture());
            lightEffect.Parameters["NormalRT"].SetValue(_normalEmissiveRT.GetTexture());
            lightEffect.Parameters["DepthRT"].SetValue(_depthRT.GetTexture());

            Vector2 halfPixel2sm = new Vector2(0.5f / (float)GameStartup.Width, 0.5f / (float)GameStartup.Height);
            lightEffect.Parameters["halfPixel"].SetValue(halfPixel2sm);

            lightEffect.Begin();
            lightEffect.Techniques[0].Passes[0].Begin();
            GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
            lightEffect.Techniques[0].Passes[0].End();
            lightEffect.End();
        }

        private void DrawDirectionalLight(LightDesc desc, ref ViewerDesc viewerDesc)
        {
            if (!desc.CastShadows)
            {
                DrawDirectionalLightNoShadows(desc, ref viewerDesc);
                return;
            }

            // Draw directional light with PSSM shadows
            bool UsePSSM = false;
            bool UsePSM = true;
            Effect lightEffect = null;
            lightEffect = _directionalLightShadowEffect;
            lightEffect.Parameters["lightDirection"].SetValue(desc.Direction);
            lightEffect.Parameters["diffuseColor"].SetValue(desc.Diffuse.ToVector3());
            lightEffect.Parameters["lightIntensity"].SetValue(desc.Intensity);
            lightEffect.Parameters["InvertViewProjection"].SetValue(Matrix.Invert(_view * _projection));
            lightEffect.Parameters["cameraPosition"].SetValue(viewerDesc.Location);

            Vector2 halfPixel = new Vector2(0.5f / _shadowMap.Width, 0.5f / _shadowMap.Height);
            lightEffect.Parameters["halfPixel"].SetValue(halfPixel);

            lightEffect.Parameters["ColorRT"].SetValue(_colorRT.GetTexture());
            lightEffect.Parameters["NormalRT"].SetValue(_normalEmissiveRT.GetTexture());
            lightEffect.Parameters["DepthRT"].SetValue(_depthRT.GetTexture());

            if (UsePSM)
            {
                Matrix lightView;
                Matrix lightProjection;

                float clippedFarClip = (viewerDesc.ShadowBox.Max - viewerDesc.ShadowBox.Min).Length();
                lightProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(60.0f), 1.0f, 1.0f, clippedFarClip * 2.2f);
                
                Vector3 newLightLoc = viewerDesc.ShadowBox.Min + (viewerDesc.ShadowBox.Max - viewerDesc.ShadowBox.Min) * 0.5f;
                newLightLoc.Y = viewerDesc.ShadowBox.Max.Y;
                newLightLoc -= desc.Direction * clippedFarClip * 0.5f;

                lightView = Matrix.CreateLookAt(newLightLoc, newLightLoc + desc.Direction, desc.Up);

                Matrix tsm = TSM.ComputeTsmMatrix(ref viewerDesc, lightView, lightProjection, 8.0f, 40.0f, _shadowMap.Width);
                lightView = lightView * lightProjection;// *tsm;
                lightProjection = Matrix.Identity;
                _workingFrustum.Matrix = lightView * lightProjection;
                BoundingFrustum lightFrustum = _workingFrustum;

                GameStartup.Graphics.SetRenderTarget(0, _shadowMap);
                GameStartup.Graphics.RenderState.AlphaBlendEnable = false;
                GameStartup.Graphics.RenderState.DepthBufferEnable = true;

                foreach (MeshResource m in viewerDesc.MeshesToRender)
                {
                    if (m.CastShadows && lightFrustum.Contains(m.BoundingSphere) != ContainmentType.Disjoint)
                    {
                        DrawMesh(m, desc.Location, ref lightView, ref lightProjection, EDrawMode.DepthOnly);
                    }
                }

                // Blur for vsm
                //for (int i = 0; i < 2; i++)
                {
                    GameStartup.Graphics.SetRenderTarget(0, _blurredShadowMap);
                    _boxBlurEffect.CurrentTechnique = _boxBlurEffect.Techniques[0];

                    _boxBlurEffect.Parameters["OriginalMap"].SetValue(_shadowMap.GetTexture());
                    halfPixel = new Vector2(0.5f / (float)GameStartup.Width, 0.5f / (float)GameStartup.Height);
                    _boxBlurEffect.Parameters["halfPixel"].SetValue(halfPixel);
                    _boxBlurEffect.Begin();
                    _boxBlurEffect.Techniques[0].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    _boxBlurEffect.Techniques[0].Passes[0].End();
                    _boxBlurEffect.End();

                    GameStartup.Graphics.SetRenderTarget(0, _shadowMap);
                    _boxBlurEffect.CurrentTechnique = _boxBlurEffect.Techniques[1];
                    _boxBlurEffect.Parameters["OriginalMap"].SetValue(_blurredShadowMap.GetTexture());
                    _boxBlurEffect.Parameters["halfPixel"].SetValue(halfPixel);
                    _boxBlurEffect.Begin();
                    _boxBlurEffect.Techniques[1].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    _boxBlurEffect.Techniques[1].Passes[0].End();
                    _boxBlurEffect.End();
                }

                GameStartup.Graphics.SetRenderTarget(0, _accumulatedLightingRT);
                GameStartup.Graphics.RenderState.AlphaBlendEnable = true;
                GameStartup.Graphics.RenderState.DepthBufferEnable = false;

                lightEffect.Parameters["lightViewProjection"].SetValue(lightView * lightProjection);

                Matrix viewProjection = lightView * lightProjection;


                Matrix texAdj = new Matrix(0.5f, 0.0f, 0.0f, 0.0f,
                                            0.0f, -0.5f, 0.0f, 0.0f,
                                            0.0f, 0.0f, 1.0f, 0.0f,
                                            0.5f + (0.5f / _shadowMap.Width), 0.5f + (0.5f / _shadowMap.Height), 0.0f, 1.0f);

                viewProjection *= texAdj;
                lightEffect.Parameters["lightTextureProjection"].SetValue(viewProjection);
                lightEffect.Parameters["lightPosition"].SetValue(lightView.Translation);

                lightEffect.Parameters["ShadowRT"].SetValue(_shadowMap.GetTexture());
                lightEffect.Begin();
                lightEffect.Techniques[0].Passes[0].Begin();
                GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                lightEffect.Techniques[0].Passes[0].End();
                lightEffect.End();
            }else
            if (UsePSSM)
            {
                // PSSM predifines
                float clippedFarClip = (viewerDesc.MeshesBox.Max - viewerDesc.MeshesBox.Min).Length() * 0.5f;
                Matrix clippedCameraProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(viewerDesc.Fov), viewerDesc.AspectRatio
                    , viewerDesc.NearClip, clippedFarClip);
                BoundingFrustum clippedCameraFrustum = new BoundingFrustum(viewerDesc.View * clippedCameraProjection);
                Vector3[] cameraFrustumCorners = clippedCameraFrustum.GetCorners();

                float cameraFarToNear = clippedFarClip / viewerDesc.NearClip;

                desc.Location = -desc.Direction * clippedFarClip;

                float splitSchemeLambda = 0.5f;
                int splitCount = 3;
                float[] splitDistances = new float[splitCount + 1];

                float splitCountDiv = 1.0f / ((float)splitCount);

                for (int i = 0; i < splitCount; i++)
                {
                    float idm = i * splitCountDiv;
                    float log = (float)(viewerDesc.NearClip * Math.Pow(cameraFarToNear, idm));
                    float uniform = viewerDesc.NearClip + (clippedFarClip - viewerDesc.NearClip) * idm;
                    splitDistances[i] = log * splitSchemeLambda + uniform * (1 - splitSchemeLambda);
                }
                splitDistances[0] = viewerDesc.NearClip;
                splitDistances[splitCount] = clippedFarClip;

                // PSSM passes
                for (int i = 0; i < splitCount; i++)
                {
                    Matrix lightView;
                    Matrix lightProjection;

                    desc.Near = splitDistances[i];
                    desc.Far = splitDistances[i + 1];

                    clippedCameraProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(viewerDesc.Fov), viewerDesc.AspectRatio
                        , splitDistances[i], splitDistances[i + 1]);
                    clippedCameraFrustum = new BoundingFrustum(viewerDesc.View * clippedCameraProjection);
                    cameraFrustumCorners = clippedCameraFrustum.GetCorners();

                    CalculateLightForFrustum(out lightView, out lightProjection, cameraFrustumCorners, desc);

                    GameStartup.Graphics.SetRenderTarget(0, _shadowMap);
                    GameStartup.Graphics.RenderState.AlphaBlendEnable = false;
                    GameStartup.Graphics.RenderState.DepthBufferEnable = true;

                    foreach (MeshResource m in viewerDesc.MeshesToRender)
                    {
                        if (m.CastShadows)// && clippedCameraFrustum.Contains(m.BoundingBox) != ContainmentType.Disjoint)
                        {
                            DrawMesh(m, desc.Location, ref lightView, ref lightProjection, EDrawMode.DepthOnly);
                        }
                    }

                    GameStartup.Graphics.SetRenderTarget(0, _accumulatedLightingRT);
                    GameStartup.Graphics.RenderState.AlphaBlendEnable = true;
                    GameStartup.Graphics.RenderState.DepthBufferEnable = false;

                    lightEffect.Parameters["lightViewProjection"].SetValue(lightView * lightProjection);

                    Matrix viewProjection = lightView * lightProjection;


                    Matrix texAdj = new Matrix(0.5f, 0.0f, 0.0f, 0.0f,
                                                0.0f, -0.5f, 0.0f, 0.0f,
                                                0.0f, 0.0f, 1.0f, 0.0f,
                                                0.5f + (0.5f / _shadowMap.Width), 0.5f + (0.5f / _shadowMap.Height), 0.0f, 1.0f);

                    viewProjection *= texAdj;
                    lightEffect.Parameters["lightTextureProjection"].SetValue(viewProjection);
                    lightEffect.Parameters["lightPosition"].SetValue(lightView.Translation);

                    lightEffect.Parameters["ShadowRT"].SetValue(_shadowMap.GetTexture());
                    lightEffect.Begin();
                    lightEffect.Techniques[0].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    lightEffect.Techniques[0].Passes[0].End();
                    lightEffect.End();
                }
            }
            else
            {
                Matrix lightView;
                Matrix lightProjection;

                float clippedFarClip = (viewerDesc.ShadowBox.Max - viewerDesc.ShadowBox.Min).Length();
                lightProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(60.0f), 1.0f, 1.0f, clippedFarClip);//Matrix.CreateOrthographic(4096, 4096, 1.0f, clippedFarClip * 0.5f);
                lightView = Matrix.CreateLookAt(viewerDesc.Location - desc.Direction * clippedFarClip * 0.1f, 
                    viewerDesc.Location + desc.Direction, desc.Up);

                _workingFrustum.Matrix = lightView * lightProjection;
                BoundingFrustum lightFrustum = _workingFrustum;

                GameStartup.Graphics.SetRenderTarget(0, _shadowMap);
                GameStartup.Graphics.RenderState.AlphaBlendEnable = false;
                GameStartup.Graphics.RenderState.DepthBufferEnable = true;

                foreach (MeshResource m in viewerDesc.MeshesToRender)
                {
                    if (m.CastShadows && lightFrustum.Contains(m.BoundingSphere) != ContainmentType.Disjoint)
                    {
                        DrawMesh(m, desc.Location, ref lightView, ref lightProjection, EDrawMode.DepthOnly);
                    }
                }

                // Blur for vsm
                //for (int i = 0; i < 2; i++)
                {
                    GameStartup.Graphics.SetRenderTarget(0, _blurredShadowMap);
                    _boxBlurEffect.CurrentTechnique = _boxBlurEffect.Techniques[0];

                    _boxBlurEffect.Parameters["OriginalMap"].SetValue(_shadowMap.GetTexture());
                    halfPixel = new Vector2(0.5f / (float)GameStartup.Width, 0.5f / (float)GameStartup.Height);
                    _boxBlurEffect.Parameters["halfPixel"].SetValue(halfPixel);
                    _boxBlurEffect.Begin();
                    _boxBlurEffect.Techniques[0].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    _boxBlurEffect.Techniques[0].Passes[0].End();
                    _boxBlurEffect.End();

                    GameStartup.Graphics.SetRenderTarget(0, _shadowMap);
                    _boxBlurEffect.CurrentTechnique = _boxBlurEffect.Techniques[1];
                    _boxBlurEffect.Parameters["OriginalMap"].SetValue(_blurredShadowMap.GetTexture());
                    _boxBlurEffect.Parameters["halfPixel"].SetValue(halfPixel);
                    _boxBlurEffect.Begin();
                    _boxBlurEffect.Techniques[1].Passes[0].Begin();
                    GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                    _boxBlurEffect.Techniques[1].Passes[0].End();
                    _boxBlurEffect.End();
                }

                GameStartup.Graphics.SetRenderTarget(0, _accumulatedLightingRT);
                GameStartup.Graphics.RenderState.AlphaBlendEnable = true;
                GameStartup.Graphics.RenderState.DepthBufferEnable = false;

                lightEffect.Parameters["lightViewProjection"].SetValue(lightView * lightProjection);

                Matrix viewProjection = lightView * lightProjection;


                Matrix texAdj = new Matrix(0.5f, 0.0f, 0.0f, 0.0f,
                                            0.0f, -0.5f, 0.0f, 0.0f,
                                            0.0f, 0.0f, 1.0f, 0.0f,
                                            0.5f + (0.5f / _shadowMap.Width), 0.5f + (0.5f / _shadowMap.Height), 0.0f, 1.0f);

                viewProjection *= texAdj;
                lightEffect.Parameters["lightTextureProjection"].SetValue(viewProjection);
                lightEffect.Parameters["lightPosition"].SetValue(lightView.Translation);

                lightEffect.Parameters["ShadowRT"].SetValue(_shadowMap.GetTexture());
                lightEffect.Begin();
                lightEffect.Techniques[0].Passes[0].Begin();
                GameStartup.QuadRenderer.Render(Vector2.One * -1, Vector2.One);
                lightEffect.Techniques[0].Passes[0].End();
                lightEffect.End();
            }
        }
    }
}
